﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using RelationExpertSystem.Relation;

namespace RelationExpertSystem
{
    public delegate void RelationAddedDelegate(RelationEventArgs args);

    public class Person
    {
        public string Name { get; set; }
        public Sex Sex { get; set; }
        public Person Father { get; internal set; }
        public Person Mother { get; internal set; }
        public Person Spouse { get; internal set; }
        //public DateTime Birthdate { get; private set; } // TODO: add older/younger/twin functionality
        public RelationAddedDelegate RelationAddedCallback { get; set; } // can only notify one object

        Dictionary<IRelation, HashSet<Person>> _relatives;

        public event EventHandler<RelationEventArgs> NewRelationAdded;
        private void RaiseNewRelationAdded(RelationEventArgs args)
        {
            EventHandler<RelationEventArgs> eh = NewRelationAdded;
            if (eh != null)
                eh(this, args);

            RelationAddedDelegate rd = RelationAddedCallback;
            if (rd != null)
                rd(args);
        }

        public Person(string name, Sex sex)
            : this(name, sex, null, null)
        { }

        public Person(string name, Sex sex, Person father, Person mother)
            : this (name, sex, father, mother, null)
        { }

        public Person(string name, Sex sex, Person father, Person mother, Person Spouse)
        {
            this.Name = name;
            this.Sex = sex;
            this.Father = father;
            this.Mother = mother;
            this.Spouse = Spouse;

            _relatives = new Dictionary<IRelation, HashSet<Person>>();
        }

        public void AddRelation(IRelation relation, Person person)
        {
            if (person == this || relation == null || Person.IsNull(person))
                return;

            if (relation == Relation.Mother.Instance)
                this.Mother = person;
            else if (relation == Relation.Father.Instance)
                this.Father = person;
            else if (relation == Relation.Spouse.Instance)
            {
                if (!Person.IsNull(this.Spouse))
                {
                    // renew vows?
                    if (this.Spouse == person)
                        return;

                    // divorced and remarried
                    Person exSpouse = this.Spouse;
                    this.AddRelation(Ex_Spouse.Instance, exSpouse);
                    exSpouse.AddRelation(Ex_Spouse.Instance, this);
                }
                this.Spouse = person;
                RaiseNewRelationAdded(new RelationEventArgs(this, person, relation));
            }
            else if (relation == Ex_Spouse.Instance)
            {
                if (this.Spouse == person)
                    this.Spouse = null; // divorced

                AddRelationToDictionary(relation, person);
            }
            else
            {
                AddRelationToDictionary(relation, person);
            }
        }

        private void AddRelationToDictionary(IRelation relation, Person person)
        {
            if (!_relatives.ContainsKey(relation))
                _relatives.Add(relation, new HashSet<Person>());

            if (!_relatives[relation].Contains(person))
            {
                _relatives[relation].Add(person);
                RaiseNewRelationAdded(new RelationEventArgs(this, person, relation));
            }
        }

        private IEnumerable<IRelation> GetRelations(Person otherPerson)
        {
            if (otherPerson == this)
                return new HashSet<IRelation>(new IRelation[] { Self.Instance });

            HashSet<IRelation> relations = new HashSet<IRelation>();
            if (otherPerson == Father)
                relations.Add(Relation.Father.Instance);
            if (otherPerson == Mother)
                relations.Add(Relation.Mother.Instance);
            if (otherPerson == Spouse)
                relations.Add(Relation.Spouse.Instance);

            foreach (IRelation relation in _relatives.Keys)
            {
                if (_relatives[relation].Contains(otherPerson))
                    relations.Add(relation);
            }

            return relations;
        }

        private IEnumerable<Person> GetRelatives(IRelation relation)
        {
            HashSet<Person> relatives = new HashSet<Person>();
            if (relation == Relation.Father.Instance)
            {
                if (!Person.IsNull(this.Father))
                    relatives.Add(Father);

                return relatives;
            }
            else if (relation == Relation.Mother.Instance)
            {
                if (!Person.IsNull(this.Mother))
                    relatives.Add(Mother);

                return relatives;
            }
            else if (relation == Relation.Spouse.Instance)
            {
                if (!Person.IsNull(this.Spouse))
                    relatives.Add(Spouse);

                return relatives;
            }

            if (_relatives.ContainsKey(relation))
            {
                HashSet<Person> copy = new HashSet<Person>();
                foreach (Person p in _relatives[relation])
                    copy.Add(p);

                return copy;
            }

            return relatives;
        }

        /// <summary>
        /// Returns if it is known that the specified person is the _relation_ of this person.
        /// </summary>
        /// <param name="relation"></param>
        /// <param name="otherPerson"></param>
        /// <returns></returns>
        public bool KnownRelation(IRelation relation, Person otherPerson)
        {
            // TODO: consider changing this to return bool? so that
            //   false is "known to be not related in this way"
            //   null is "relation state unknown"

            if (Person.IsNull(otherPerson))
                return false;

            if (relation == Self.Instance)
                return otherPerson == this;
            else if (relation == Relation.Father.Instance)
                return otherPerson == Father;
            else if (relation == Relation.Mother.Instance)
                return otherPerson == Mother;
            else if (relation == Relation.Spouse.Instance)
                return otherPerson == Spouse;
            else if (_relatives.ContainsKey(relation))
                return _relatives[relation].Contains(otherPerson);

            return false;
        }

        public void RemoveRelation(IRelation relation, Person otherPerson)
        {
            if (Person.IsNull(otherPerson) || relation == Self.Instance)
                return;

            if (relation == Relation.Father.Instance && this.Father == otherPerson)
                this.Father = null;
            else if (relation == Relation.Mother.Instance && this.Mother == otherPerson)
                this.Mother = null;
            else if (relation == Relation.Spouse.Instance && this.Spouse == otherPerson)
                this.Spouse = null;
            else if (_relatives.ContainsKey(relation) && _relatives[relation].Contains(otherPerson))
                _relatives[relation].Remove(otherPerson);
        }

        public void ClearRelations()
        {
            //this.Father = null;
            //this.Mother = null;
            //this.Spouse = null;
            _relatives.Clear();
        }

        public static bool IsNull(Person p)
        {
            return (object)p == null;
        }

        public static bool operator ==(Person a, Person b)
        {
            // If either/both are null, return false
            if (((object)a == null) || ((object)b == null))
            {
                return false;
            }

            return System.Object.ReferenceEquals(a, b);
        }

        public static bool operator !=(Person a, Person b)
        {
            return !(a == b);
        }

        public override string ToString()
        {
            return String.Format("{0} ({1})", this.Name, this.Sex.ToString());
        }
    }
}
